/+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 + SoulFind - Free SoulSeek server                                           +
 +                                                                           +
 + Copyright (C) 2005 SeeSchloss <seeschloss@seeschloss.org>                 +
 +                                                                           +
 + This  program  is free software ; you can  redistribute it  and/or modify +
 + it under  the  terms of  the GNU General Public License  as published  by +
 + the  Free  Software  Foundation ;  either  version  2 of  the License, or +
 + (at your option) any later version.                                       +
 +                                                                           +
 + This  program  is  distributed  in the  hope  that  it  will  be  useful, +
 + but   WITHOUT  ANY  WARRANTY ;  without  even  the  implied  warranty  of +
 + MERCHANTABILITY   or   FITNESS   FOR   A   PARTICULAR  PURPOSE.  See  the +
 + GNU General Public License for more details.                              +
 +                                                                           +
 + You  should  have  received  a  copy  of  the  GNU General Public License +
 + along   with  this  program ;  if  not,  write   to   the  Free  Software +
 + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA +
 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++/


module client;

import defines;

private import messages;
private import server;
private import room;
private import pm;
private import db;
private import message_codes;

import std.stream, std.cstream;
import std.socketstream;
import std.thread;
import std.string;
import std.socket;
import std.date  ;
import std.stdio  ;

import std.c.process;

class User
	{
	// some attributes...
	char[]	username;
	char[]	password;
	uint	cversion;

	uint	address;
	uint	port;

	bit	admin;

	uint	privileges;		// in seconds
	long	last_checked_privileges;// privileges length is counted from this date
	uint	speed;			// received in B/s, sent in kB/s
	uint	download_number;
	uint	something;
	uint	shared_files;
	uint	shared_folders;
	uint	slots_full;

	int	status;			// 0,1,2
	bit	loggedin;
	long	connected_at;		// in ticks
	long	last_message_date;	// in ticks

	char[][char[]]	things_he_likes;
	char[][char[]]	things_he_hates;

	Stream	stream;
	Socket	socket;
	Server	server;

	ubyte[] boeuf;

	// constructors
	this (Server serv, Socket s, uint address)
		{
		this.server            = serv;
		this.socket            = s;
		if (std.system.endian == std.system.Endian.BigEndian)
			this.stream    = new EndianStream (new SocketStream (s), std.system.Endian.LittleEndian);
		else
			this.stream    = new SocketStream (s);
		this.address           = address;
		this.loggedin          = false;
		this.admin             = false;
		this.connected_at      = getUTCtime();
		this.last_message_date = getUTCtime();
		}
	
	this () {}
	
	// misc
	char[] list_joined_rooms ()
		{
		char[] list;
		foreach (Room r ; joined_rooms ())
			{
			list ~= r.name ~ " ";
			}
		return list;
		}
	
	char[] print_privileges ()
		{
		return this.privileges > 0 ? print_length (this.privileges) : "None";
		}

	void calc_speed (uint speed)
		{
		if (this.download_number == 0)
			{
			this.download_number = 1;
			this.speed = speed;
			}
		else
			{
			this.speed = (this.speed*download_number + speed)/(download_number + 1);
			this.download_number++;
			}

		send_to_watching (new SGetUserStats (this.username, this.speed, this.download_number, this.something, this.shared_files, this.shared_folders));

		server.db.user_update_field (this.username, "speed", this.speed);
		}
	
	void set_shared_files (uint files)
		{
		this.shared_files = files;
		server.db.user_update_field (this.username, "files", this.shared_files);
		}
	
	void set_shared_folders (uint folders)
		{
		this.shared_folders = folders;
		server.db.user_update_field (this.username, "folders", this.shared_folders);
		}
	
	void send_pm (PM pm)
		{
		this.send_message (new SMessageUser (pm.id, pm.timestamp, pm.from, pm.content));
		}
	
	// privileges
	void add_privileges (uint privileges)
		{
		debug (2) printf ("Adding %d seconds of privileges to user %.*s\n", privileges, username);
		this.privileges += privileges;
		if (this.privileges < 0) this.privileges = 0;
		debug (2) writefln ("Now %d seconds.", this.privileges);
		server.db.user_update_field (this.username, "privileges", this.privileges);
		}
	
	void remove_privileges (uint privileges)
		{
		debug (2) printf ("Removing %d seconds of privileges from user %.*s\n", privileges, username);
		this.privileges -= privileges;
		if (this.privileges < 0) this.privileges = 0;
		debug (2) writefln ("Now %d seconds.", this.privileges);
		server.db.user_update_field (this.username, "privileges", this.privileges);
		}
	
	void update_privileges ()
		{
		long now = getUTCtime () / TicksPerSecond;
		this.privileges -= now - this.last_checked_privileges;
		if (this.privileges < 0) this.privileges = 0;
		this.last_checked_privileges = now;
		server.db.user_update_field (this.username, "privileges", this.privileges);
		}
	
	uint get_privileges ()
		{
		update_privileges ();
		return this.privileges;
		}
	
	// things I like
	void add_thing_he_likes (char[] thing)
		{
		if (!this.likes (thing))
			{
			things_he_likes[thing] = thing;
			debug (2) writefln ("new like %s.", thing);
// 			debug (2) writefln ("Likes: %d", things_he_likes.length);
			}
		}
	
	void del_thing_he_likes (char[] thing)
		{
		if (this.likes (thing))
			{
			things_he_likes.remove (thing);
			}
		}
	
	void add_thing_he_hates (char[] thing)
		{
		if (!this.hates (thing))
			{
			things_he_hates[thing] = thing;
			debug (2) writefln (" new hate %s.", thing);
			}
		}
	
	void del_thing_he_hates (char[] thing)
		{
		if (this.hates (thing))
			{
			things_he_hates.remove (thing);
			}
		}
	
	bit likes (char[] thing)
		{
		return (!(!(thing in things_he_likes)));
		}
	
	bit hates (char[] thing)
		{
		return (!(!(thing in things_he_hates)));
		}
	
	uint[char[]] get_recommendations ()
		{
		uint[char[]] list;

		foreach (User u ; server.users ())
			{
			if (!(this is u)) {
				uint weight = 0;
				foreach (char[] thing ; this.things_he_likes)
					{
					if (u.likes (thing))
						{
						weight++;
						}
					if (u.hates (thing) && weight > 0)
						{
						weight--;
						}
					}
				foreach (char[] thing ; things_he_hates)
					{
					if (u.hates (thing))
						{
						weight++;
						}
					if (u.likes (thing) && weight > 0)
						{
						weight--;
						}
					}
				if (weight > 0) foreach (char[] thing ; u.things_he_likes)
					{
					list[thing] += weight;
					}
				}
			};
		return list;
		}
	
	uint[char[]] get_similar_users ()
		{
		uint[char[]] users;

		foreach (User u ; server.users ())
			{
			if (!(this is u)) {
				uint weight = 0;
				foreach (char[] thing ; things_he_likes)
					{
					if (u.likes (thing))
						{
						weight++;
						}
					if (u.hates (thing) && weight > 0)
						{
						weight--;
						}
					}
				foreach (char[] thing ; things_he_hates)
					{
					if (u.hates (thing))
						{
						weight++;
						}
					if (u.likes (thing) && weight > 0)
						{
						weight--;
						}
					}
				if (weight > 0) users[u.username] = weight;
				}
			};

		return users;
		}
	
	uint[char[]] get_item_recommendations (char[] item)
		{
		uint[char[]] list;

		foreach (User u ; server.users ())
			{
			if (!(this is u)) {
				uint weight = 0;
				if (u.likes (item))
					{
					weight++;
					}
				if (u.hates (item) && weight > 0)
					{
					weight--;
					}
	
				if (weight > 0) foreach (char[] thing ; u.things_he_likes)
					{
					list[thing] += weight;
					}
				}
			};
		return list;
		}
	
	char[][] get_item_similar_users (char[] item)
		{
		char[][] list;

		foreach (User u ; server.users ())
			{
			if (!(this is u)) {
				if (u.likes (item))
					{
					list ~= u.username;
					}
				}
			};

		return list;
		}
	
	// watching
	char[] list_watching ()
		{
		char[] list;

		debug (5) writefln ("Hmmm x3");

		foreach (User user ; watching ())
			{
			list ~= user.username ~ " ";
			}
		debug (5) writefln ("Hmmm x4");
		return list;
		}
	
	char[] list_watched_by ()
		{
		char[] list;
		foreach (User user ; watched_by ())
			{
			list ~= user.username ~ " ";
			}
		return list;
		}
	
	void send_to_watching (Message m)
		{
// 		debug (3) writef ("Sending message code ", m.code, " to watched users\n");
		foreach (User user ; this.watched_by ())
			{
// 			if (!(user.username is username)) {
			if (user.loggedin) {
// 			debug (3) printf ("%s\n", user.username);
			user.send_message (m);
 			};
			}
// 		debug (3) writefln ();
		}
	
	void set_status (int status)
		{
		this.status = status;
		this.send_to_watching (new SGetUserStatus (this.username, this.status));
		}
	
	
	// watchlist, etc
	char[][char[]] watch_list;	// watch_list[username] = username
	
	void watch (char[] username)
		{
		watch_list[username] = username;
		}
	
	void unwatch (char[] username)
		{
		if (username in watch_list)
			{
			watch_list.remove (username);
			}
		}
	
	User[] watched_by ()
		{
		User[] list;
		foreach (User user ; server.users ())
			{
			debug (5) writefln ("Hmmm x7");
			if (user.watching().length > 0 && this.username in user.watching ()) // && user !is this 
				list ~= user;
			debug (5) writefln ("Hmmm x8");
			}
		return list;
		}
	
	User[char[]] watching ()
		{
		User[char[]] list;
		if (watch_list.length > 0) foreach (char[] username ; watch_list)
			{
			if (server.find_user (username)) list[username] = server.get_user (username);
			}
		if (joined_rooms().length > 0) foreach (Room room ; joined_rooms ())
			{
			foreach (User user ; room.users ())
				{
				list[user.username] = user;
				}
			}
		return list;
		}
	
	// rooms, etc
	char[][char[]] room_list;	// room_list[roomname] = roomname

	void join_room (char[] roomname)
		{
		room_list[roomname] = roomname;
		}
	
	void leave_room (char[] roomname)
		{
		if (roomname in room_list) room_list.remove (roomname);
		}
	
	Room[] joined_rooms ()
		{
		Room[] tmp;
		if (room_list.length > 0) foreach (char[] roomname ; room_list)
			{
			if (Room.find_room (roomname)) tmp ~= Room.get_room (roomname);
			}
		return tmp;
		}
	
	// messages
	int run ()
		{
		while (recv_message ()) {}

		if (server.find_user (this.username)) server.del_user (this);

		exit ();
		return 0;
		}

	void send_message (Message m)
		{
		boeuf ~= m.toBytes ();

		try
			{
			socket.blocking = false;
			stream.write (cast (uint) boeuf.length);
			stream.write (cast (ubyte[]) boeuf);
			socket.blocking = true;
			debug (4) printf (cast(char*)( "Sent %d bytes to user " ~ blue ~ "%.*s" ~ black ~ "\n", boeuf.length, this.username));
			boeuf.length = 0;
			}
		catch (Exception e)
			{
			printf ("%.*s : %.*s\n", this.username, e);
			}
		}
	
	bit recv_message ()
		{
		try
			{
			uint length; stream.read (length);
			
			if (length < 0 || length > server.max_message_size)
				{ // message is probably bogus, let's disconnect the user
				return false;
				}
			
			ubyte[] bœuf; bœuf.length = length;

			last_message_date = getUTCtime ();

			uint read = stream.readBlock (cast(void*)bœuf, length);

			if (read != length)
				{
				debug (1) derr.writefln ("Couldn't read the whole message (", read, "/", length,
				          ")... the client is probably disconnected");
				return false;
				}

			MemoryStream ms = new MemoryStream (bœuf);
		
			return proc_message (ms);
			}
		catch (ReadException e)
			{
			debug (2) derr.writefln (username, " : ", e);

			return false;
			}
		}
	
	bit proc_message (Stream s)
		{
		uint code;
		s.read (code);
		debug (3) if (code != 32 && code < message_name.length) writefln ("Received message ", blue, message_name[code], black, " (code ", blue, code, black ~ ")");
		
		if (!loggedin && code != Login) return false;
		if (loggedin  && code == Login) return true;

		switch (code)
			{
			case Login:
				debug (1) writef ("User logging in : ");
				ULogin o = new ULogin (s);
				char[] error;


				if (server.db.conf_get_int ("case_insensitive"))
					{
					char[] realname = server.db.get_insensitive_username (o.name);
					if (realname) o.name = realname;
					}

				if (!server.check_login (o.name, o.pass, o.vers, error))
					{
					debug (1) writefln ("%s : Impossible to login (%s)", o.name, error);
					send_message (new SLogin (0, error, 0));
					return false;
					}
				else if (server.find_user (o.name) && server.get_user (o.name).loggedin)
					{
					debug (1) writefln ("%s : Already logged in", o.name);
					User u = server.get_user (o.name);
					u.send_message (new SRelogged ());
					u.exit ();
					}

				debug (1) writefln (blue ~ "%s" ~ black ~ ", version %d", o.name, o.vers);
				return (this.login (o));
				break;
			case SetWaitPort:
				USetWaitPort o = new USetWaitPort (s);
				this.port = o.port;
				break;
			case GetPeerAddress:
				UGetPeerAddress o = new UGetPeerAddress (s);
				
				if (server.find_user (o.user))
					{
					User user = server.get_user (o.user);
					send_message (new SGetPeerAddress (user.username, user.address, user.port));
					}
				else
					{
					send_message (new SGetPeerAddress (o.user, 0, 0));
					}
				break;
			case UserExists: // see message_codes.d for some explanation about these codes
			case AddUser:
				UAddUser o = new UAddUser (s);
				
				if (server.db.user_exists (o.user))
					{
					send_message (new SUserExists (o.user, true));

					uint speed, download_number, something, shared_files, shared_folders;
					server.db.get_user (o.user, speed, download_number, something, shared_files, shared_folders);
					send_message (new SGetUserStats (o.user, speed, download_number, something, shared_files, shared_folders));
					watch (o.user);
					}
				else if (o.user == server.server_user)
					{
					send_message (new SUserExists (o.user, true));

					send_message (new SGetUserStats (o.user, 0, 0, 0, 0, 0));
					}
				else
					{
					send_message (new SUserExists (o.user, false));
					}
				break;
			case GetUserStatus:
				UGetUserStatus o = new UGetUserStatus (s);

				debug (2) writef ("Sending ", o.user, "'s status... ");
				if (server.find_user (o.user))
					{	// user is online
					debug (2) writefln ("online.");
					send_message (new SGetUserStatus (o.user, server.get_user (o.user).status));
					}
				else if (server.db.user_exists (o.user))
					{	// user is offline but exists
					debug (2) writefln ("offline.");
					send_message (new SGetUserStatus (o.user, 0));
					}
				else if (o.user == server.server_user)
					{	// user is the server administration interface
					debug (2) writefln ("server (online)");
					send_message (new SGetUserStatus (o.user, 2));
					}
				else
					{	// user doesn't exist
					debug (2) writefln ("doesn't exist.");
					send_message (new SUserExists (o.user, 0));
					}
				break;
			case SayChatroom:
				USayChatroom o = new USayChatroom (s);
				if (Room.find_room (o.room))
					Room.get_room (o.room).say (this.username, o.string);
				break;
			case JoinRoom:
				UJoinRoom o = new UJoinRoom (s);

				if (server.check_string (o.room)) Room.join_room (o.room, this);
				break;
			case LeaveRoom:
				ULeaveRoom o = new ULeaveRoom (s);

				if (Room.find_room (o.room)) Room.get_room (o.room).leave (this);
				this.leave_room (o.room);
				
				send_message (new SLeaveRoom (o.room));
				break;
			case ConnectToPeer:
				UConnectToPeer o = new UConnectToPeer (s);

				if (server.find_user (o.user))
					{
					User user = server.get_user (o.user);
					InternetAddress ia = new InternetAddress (user.address, user.port);
					debug (2) writefln (this.username, " cannot connect to ", o.user, "/", ia.toString(), ", asking us to tell the other...");
					user.send_message (new SConnectToPeer (user.username, o.type, user.address, user.port, o.token));
					}
				break;
			case MessageUser:
				UMessageUser o = new UMessageUser (s);

				if (this.admin && o.user == server.server_user)
					{
					server.admin_message (this, o.message);
					}
				else if (server.find_user (o.user))
					{ // user is connected
					PM pm = new PM (o.message, this.username, o.user);
					User dest = server.get_user (o.user);
					PM.add_pm (pm);
					dest.send_pm (pm);
					}
				else if (server.db.user_exists (o.user))
					{ // user is not connected but exists
					if (PM.nb_messages (o.user) < server.max_offline_pms)
						{
						PM pm = new PM (o.message, this.username, o.user);
						PM.add_pm (pm);
						}
					}
				break;
			case MessageAcked:
				UMessageAcked o = new UMessageAcked (s);
				PM.del_pm (o.id);
				break;
			case FileSearch:
				UFileSearch o = new UFileSearch (s);
				server.do_FileSearch (o.token, o.strng, this.username);
				break;
			case WishlistSearch:
				UFileSearch o = new UFileSearch (s);
				server.do_FileSearch (o.token, o.strng, this.username);
				break;
			case SetStatus:
				USetStatus o = new USetStatus (s);
				set_status (o.status);
				break;
			case ServerPing:
				send_message (new SServerPing ());
				break;
			case SendSpeed:
				USendSpeed o = new USendSpeed (s);

				if (server.find_user (o.user))
					{
					User u = server.get_user (o.user);
					u.calc_speed (o.speed);

					debug (2) writefln ("User ", this.username, " reports a speed of ", o.speed, " B/s for user ", o.user, " (whose speed is now ", u.speed, " B/s)");
					}
				break;
			case SendUploadSpeed:
				USendUploadSpeed o = new USendUploadSpeed (s);

				if (server.find_user (this.username))
					{
					User u = server.get_user (this.username);
					u.calc_speed (o.speed);

					debug (2) writefln ("User ", this.username, " reports a speed of ", o.speed, " B/s for user ", this.username, " (whose speed is now ", u.speed, " B/s)");
					}
				break;
			case SharedFoldersFiles:
				USharedFoldersFiles o = new USharedFoldersFiles (s);
				debug (2) writefln (this.username, " is sharing ", o.nb_files, " files and ", o.nb_folders, " folders");
				this.set_shared_folders (o.nb_folders);
				this.set_shared_files (o.nb_files);

				this.send_to_watching (new SGetUserStats (this.username, this.speed, this.download_number, this.something, this.shared_files, this.shared_folders));
				break;
			case GetUserStats:
				UGetUserStats o = new UGetUserStats (s);
				
				uint speed, download_number, something, shared_files, shared_folders;
				server.db.get_user (o.user, speed, download_number, something, shared_files, shared_folders);
				send_message (new SGetUserStats (o.user, speed, download_number, something, shared_files, shared_folders));
				break;
			case UserSearch:
				UUserSearch o = new UUserSearch (s);

				server.do_UserSearch (o.token, o.query, this.username, o.user);
				break;
			case AddThingILike:
				UAddThingILike o = new UAddThingILike (s);

				this.add_thing_he_likes (o.thing);

				break;
			case RemoveThingILike:
				URemoveThingILike o = new URemoveThingILike (s);

				this.del_thing_he_likes (o.thing);

				break;
			case AddThingIHate:
				UAddThingIHate o = new UAddThingIHate (s);

				this.add_thing_he_hates (o.thing);

				break;
			case RemoveThingIHate:
				URemoveThingIHate o = new URemoveThingIHate (s);

				this.del_thing_he_hates (o.thing);

				break;
			case GetRecommendations:
				send_message (new SGetRecommendations (get_recommendations ()));
				break;
			case GlobalRecommendations:
				send_message (new SGetGlobalRecommendations (server.global_recommendations ()));
				break;
			case SimilarUsers:
				send_message (new SSimilarUsers (get_similar_users ()));
				break;
			case UserInterests:
				UUserInterests o = new UUserInterests (s);
				if (server.find_user (o.user)) {
					User u = server.get_user (o.user);
					send_message (new UUserInterests (u.username, u.things_he_likes, u.things_he_hates));

				}
				
				break;
			case RoomList:
				send_message (new SRoomList (Room.room_stats ()));
				break;
			case AdminMessage:
				if (this.admin)
					{
					UAdminMessage o = new UAdminMessage (s);

					foreach (User user ; server.users ())
						{
						user.send_message (new SAdminMessage (o.mesg));
						}
					}
				break;
			case AddToPrivileged:
				if (this.admin)
					{
					UAddToPrivileged o = new UAddToPrivileged (s);

					if (server.find_user (o.user))
						{
						server.get_user (o.user).privileges += o.time;
						}
					}
				break;
			case CheckPrivileges:
				send_message (new SCheckPrivileges (this.get_privileges ()));
				break;
			case ItemRecommendations:
				UGetItemRecommendations o = new UGetItemRecommendations (s);
				send_message (new SGetItemRecommendations (o.item, get_item_recommendations (o.item)));
				break;
			case ItemSimilarUsers:
				UItemSimilarUsers o = new UItemSimilarUsers (s);
				send_message (new SItemSimilarUsers (o.item, get_item_similar_users (o.item)));
				break;
			case SetRoomTicker:
				USetRoomTicker o = new USetRoomTicker (s);
				if (Room.find_room (o.room))
					{
					Room.get_room (o.room).add_ticker (this.username, o.tick);
					}
				break;
			case RoomSearch:
				URoomSearch o = new URoomSearch (s);

				server.do_RoomSearch (o.token, o.query, this.username, o.room);
				break;
			case UserPrivileges:
				UUserPrivileges o = new UUserPrivileges (s);
				
				if (server.find_user (o.user))
					{
					User u = server.get_user (o.user);
					send_message (new SUserPrivileges (u.username, u.privileges));
					}
				break;
			case GivePrivileges:
				UGivePrivileges o = new UGivePrivileges (s);

				if ((o.time <= this.privileges || admin) && server.find_user (o.user))
					{
					server.get_user (o.user).add_privileges (o.time*3600*24);
					if (!admin) this.remove_privileges (o.time*3600*24);
					}
				break;
			case CantConnectToPeer:
				UCantConnectToPeer o = new UCantConnectToPeer (s);

				if (server.find_user (o.user))
					{
					server.get_user (o.user).send_message (new SCantConnectToPeer (o.token));
					}
				break;
			default:
				debug (2)
					{
					writef (red, "Un-implemented message", black, "from user ", underline,
						username.length > 0 ? username : std.string.toString (address), black,
						", code ", red, code, black, " and length ", s.size (), "\n> ");
					try {writefln (s.toString ());}
					catch (Exception e) {writefln ();}
					}
				break;
			}
		return true;
		}
	
	bit login (ULogin m)
		{
		char[] message = this.server.get_motd (m.name, m.vers);
		this.username = m.name;
		this.password = m.pass;
		this.cversion = m.vers;
		send_message (new SLogin (1, message, this.address, server.db.conf_get_int("md5_ident") ? this.password : null));
		this.loggedin = true;

		if (!server.db.get_user (this.username, this.password, this.speed, this.download_number, this.shared_files, this.shared_folders, this.privileges))
			{
			throw new Exception ("User " ~ this.username ~ " does not exist.");
			}

		if (this.username in server.admins) this.admin = true;
		debug (1) if (admin) writefln ("%s is an admin.", this.username);
		server.add_user (this);

		send_message (new SRoomList (Room.room_stats ()));
		this.set_status (2);
		send_message (new SWishlistInterval ());
		
		foreach (PM pm ; PM.get_pms_for (this.username))
			{
			debug (3) writefln ("Sending offline PM (id %d) to %s", pm.id, this.username);
			send_pm (pm);
			}
		
		return true;
		}

	void exit ()
		{
		update_privileges ();
		foreach (Room room ; this.joined_rooms ())
			{
			room.leave (this);
			}
		this.loggedin = false;
		this.set_status (0);
		debug (1) if (this.username.length > 0) writefln ("User " ~ blue ~ "%s" ~ black ~ " has quit.", username);
		}
	}
